package pl.kaczmarz.audiosteganography;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import javax.swing.JTextArea;
import javax.swing.SwingWorker;

/**
 *
 * @author Pawel
 */
public class Encoder extends SwingWorker<Void, Void> {

	private String inputFileName;
	private String outputFileName;
	private String messageFileName;
	private String keyFileName;


	private double zeroDelay;
	private double oneDelay;

	// key is set up after every encoding.
	private KeyFile key;

	private double decay;
	private double amplitude;

	private int messageRepeat;

	private JTextArea consoleOut;

	private int properBits;
	private int properBytes;

	public String getKeyFileName() {
		return keyFileName;
	}

	public void setKeyFileName(String keyFileName) {
		this.keyFileName = keyFileName;
	}

	public void setAmplitude(double amplitude) {
		this.amplitude = amplitude;
	}

	public void setDecay(double decay) {
		this.decay = decay;
	}

	public void setInputFileName(String inputFileName) {
		this.inputFileName = inputFileName;
	}

	public void setMessageFileName(String messageFileName) {
		this.messageFileName = messageFileName;
	}

	public void setMessageRepeat(int messageRepeat) {
		this.messageRepeat = messageRepeat;
	}

	public void setOneDelay(double oneDelay) {
		this.oneDelay = oneDelay;
	}

	public void setOutputFileName(String outputFileName) {
		this.outputFileName = outputFileName;
	}

	public void setZeroDelay(double zeroDelay) {
		this.zeroDelay = zeroDelay;
	}

	public double getAmplitude() {
		return amplitude;
	}

	public double getDecay() {
		return decay;
	}

	public String getInputFileName() {
		return inputFileName;
	}

	public KeyFile getKey() {
		return key;
	}

	public String getMessageFileName() {
		return messageFileName;
	}

	public int getMessageRepeat() {
		return messageRepeat;
	}

	public double getOneDelay() {
		return oneDelay;
	}

	public String getOutputFileName() {
		return outputFileName;
	}

	public double getZeroDelay() {
		return zeroDelay;
	}

	public void setConsoleOut(JTextArea consoleOut) {
		this.consoleOut = consoleOut;
	}

	public JTextArea getConsoleOut() {
		return consoleOut;
	}

	public int getProperBits() {
		return properBits;
	}

	public void setProperBits(int properBits) {
		this.properBits = properBits;
	}

	public int getProperBytes() {
		return properBytes;
	}

	public void setProperBytes(int properBytes) {
		this.properBytes = properBytes;
	}



	// delay given in ms.
	public static double echoIntensity(double amplitude,
			double decay, double delay) {
		return amplitude / (decay * delay + 1);
	}

	public static void applyEcho(WaveFile input, WaveFile output, int delay, double intensity) {
		for (int sample = delay; sample < input.getNumberOfBlocks(); ++ sample) {
			for (int channel = 0; channel < input.getNumberOfChannels(); ++channel) {
				int value = input.readSample(channel, sample);
				int postEcho = input.readSample(channel, sample - delay);
				int newValue = (int) ((value + postEcho * intensity) / (1 + intensity));
				output.writeSample(channel, sample, newValue);
			}
		}
	}

	public void createTempTracks() throws FileNotFoundException, IOException, UnsupportedWaveFormatException, UnexpectedValueException {

		String oneFileName = "temp/temp_one.wav";
		String zeroFileName = "temp/temp_zero.wav";

		try {
			Files.createDirectory(Paths.get("temp"));
		} catch (FileAlreadyExistsException e) {
			// If it already exist - it is ok.
		}

		WaveFile.copy(inputFileName, oneFileName);
		WaveFile.copy(inputFileName, zeroFileName);

		try (WaveFile oneInput = WaveFile.open(oneFileName, "rw");
				WaveFile zeroInput = WaveFile.open(zeroFileName, "rw");
				WaveFile inputFile = WaveFile.open(inputFileName, "r")) {

			int zeroDelayBlocks = inputFile.milisecondsToBlocks(zeroDelay);
			int oneDelayBlocks = inputFile.milisecondsToBlocks(oneDelay);

			double zeroIntensity = echoIntensity(amplitude, decay, zeroDelay);
			double oneIntensity = echoIntensity(amplitude, decay, oneDelay);

			WaveFile.copy(inputFileName, oneFileName);
			WaveFile.copy(inputFileName, zeroFileName);

			applyEcho(inputFile, oneInput, oneDelayBlocks, oneIntensity);
			applyEcho(inputFile, zeroInput, zeroDelayBlocks, zeroIntensity);

		}
	}

	public void encodeFromTemps() throws TooManyRetriesException, IOException, UnexpectedValueException, UnsupportedWaveFormatException {

		String oneFileName = "temp/temp_one.wav";
		String zeroFileName = "temp/temp_zero.wav";
		WaveFile.copy(zeroFileName, outputFileName);

		try (WaveFile outputFile = WaveFile.open(outputFileName, "rw");
				WaveFile oneInput = WaveFile.open(oneFileName, "r");
				WaveFile zeroInput = WaveFile.open(zeroFileName, "r");) {

			int zeroDelayBlocks = zeroInput.milisecondsToBlocks(zeroDelay);
			int oneDelayBlocks = zeroInput.milisecondsToBlocks(oneDelay);

			boolean[] messageBits = Tools.toBits(Files.readAllBytes(Paths.get(messageFileName)));

			int blockLengthBlocks = (int) (zeroInput.getNumberOfBlocks() / (messageBits.length * messageRepeat));

			if (Math.max(zeroDelayBlocks, oneDelayBlocks) * 2 > blockLengthBlocks) {
				throw new TooManyRetriesException();
			}

			//WaveFile.copy(zeroFileName, outputFileName);

			// TODO: podać długośc fadingu jako parametr.
			int fadingLengthBlocks = zeroInput.milisecondsToBlocks(0.5);
			int fadingStart = blockLengthBlocks - fadingLengthBlocks / 2;
			int fadingEnd = blockLengthBlocks + fadingLengthBlocks / 2;
			int currentBit = 0;
			int messageIteration = 1;
			int[] zeroBlock = new int[zeroInput.getNumberOfChannels()];
			int[] oneBlock = new int[oneInput.getNumberOfChannels()];
			int[] outputBlock = new int[zeroInput.getNumberOfChannels()];

			for (int offset = 0; offset < zeroInput.getNumberOfBlocks(); ++offset) {
				zeroInput.readBlock(offset, zeroBlock);
				oneInput.readBlock(offset, oneBlock);

				float zeroFilter;
				float oneFilter;

				if (currentBit < messageBits.length && messageBits.length > 0) {
					if (offset < fadingStart) {
						oneFilter = messageBits[currentBit] ? 1.f : 0.f;
					} else {
						// inside the "fading region"
						float filterStart = messageBits[currentBit] ? 1.f : 0.f;
						float filterEnd;

						if (currentBit + 1 < messageBits.length) {
							filterEnd = messageBits[currentBit + 1] ? 1.f : 0.f;
						} else {
							filterEnd = 0.f;
						}

						if (filterStart == filterEnd) {
							oneFilter = filterStart;
						} else {
							// Fading between values 0-1 or 1-0 at the length of fadingLengthBlocks
							oneFilter = filterStart + (offset - fadingStart) * (filterEnd - filterStart) / fadingLengthBlocks;
						}
					}
				} else {
					oneFilter = 0;
				}

				zeroFilter = 1.f - oneFilter;

				// apply the filters and set the result block
				for (int channel = 0; channel < zeroInput.getNumberOfChannels(); ++channel) {
					outputBlock[channel] = (int) (zeroFilter * zeroBlock[channel] + oneFilter * oneBlock[channel]);
				}

				outputFile.writeBlock(offset, outputBlock);

				// if at the end of fading region, move the fading region one block further.
				if (offset == fadingEnd) {
					++currentBit;

					if (currentBit >= messageBits.length && messageIteration < messageRepeat) {
						currentBit = 0;
						++messageIteration;
					}

					fadingStart += blockLengthBlocks;
					fadingEnd += blockLengthBlocks;
				}
			}

			// remember used settings
			key = new KeyFile();
			key.setBlockLength(zeroInput.blocksToMiliseconds(blockLengthBlocks));
			key.setMessageLength(messageBits.length);
			key.setMessageRepeat(messageRepeat);
			key.setOneDelay(oneDelay);
			key.setZeroDelay(zeroDelay);

		}
	}

	public Void doInBackground() {
		return encode();
	}

	public Void encode() {

		setProgress(0);
		setProperBits(0);
		setProperBytes(0);

		if (consoleOut != null) {
			consoleOut.setText("");
			consoleOut.append("Trwa kodowanie wiadomości...\n");
		}

		try {
			try {
				Files.createDirectory(Paths.get("temp"));
			} catch (FileAlreadyExistsException e) {
				// If it already exists - it is ok.
			}

			Decoder dec = new Decoder();

			String finalOutputFileName = getOutputFileName();

			setOutputFileName("temp/temp.wav");

			dec.setInputFileName("temp/temp.wav");
			dec.setOutputFileName("temp/temp.txt");

			byte[] messageFile = Files.readAllBytes(Paths.get(getMessageFileName()));
			boolean[] messageFileBits =Tools.toBits(messageFile);

			int maxRetries = 8;


			int retry = 0;
			int overallRetries = 0;
			double similarity = 0;

			int bestBitsSettings = 0;
			int bestBitsUltimate = 0;
			int bestBytesUltimate = 0;

			allRetries:
			while (retry < maxRetries) {

				++retry;

				int bestBitsRepeats = 0;
//				int bestBytesRepeats = 0;

				int repeats = 1;

				double zeroIntensity = 0;
				double oneIntensity = 0;

				if (consoleOut != null) {
						consoleOut.append("\n\n");
						consoleOut.append("Zastaw parametrów: " + retry + "\n");
						consoleOut.append("Losowanie klucza... \n");
				}

				do {
					setAmplitude(Math.random());
					setDecay(Math.random());

					setOneDelay(Math.random() * 7 + 1);
					setZeroDelay(Math.random() * 7 + 1);

					// calculate intensity to discard too low intensity
					zeroIntensity = Encoder.echoIntensity(getAmplitude(), getDecay(), getZeroDelay());
					oneIntensity = Encoder.echoIntensity(getAmplitude(), getDecay(), getOneDelay());

				} while (zeroIntensity < 0.1 || zeroIntensity > 0.5 || oneIntensity < 0.1 || oneIntensity > 0.5);

				setMessageRepeat(repeats);

				createTempTracks();

				setRepeat:
				while (true) {

					++overallRetries;

					setProgress((int) (100 * ((retry - 1) * 10 + Math.max(repeats, 16)) / ((double) maxRetries * 10 + 16)));

					setMessageRepeat(repeats);

					if (consoleOut != null) {
						consoleOut.append("\nAmplituda:         " + getAmplitude() + "\n");
						consoleOut.append("Opadanie:          " + getDecay() + "\n");
						consoleOut.append("Opoznienie 0:      " + getZeroDelay() + "\n");
						consoleOut.append("Opoznienie 1:      " + getOneDelay() + "\n");
						consoleOut.append("Amplituda 0:       " + Encoder.echoIntensity(getAmplitude(), getDecay(), getZeroDelay()) + "\n");
						consoleOut.append("Amplituda 1:       " + Encoder.echoIntensity(getAmplitude(), getDecay(), getOneDelay()) + "\n");
						consoleOut.append("Powtórzenia:       " + getMessageRepeat() + "\n");
					}

					try {
						encodeFromTemps();

						getKey().save("temp/temp.key.txt");

						dec.setKeyFileName("temp/temp.key.txt");

						dec.decode();

						// compare result of encoding
						byte[] decodedMessage = Files.readAllBytes(Paths.get("temp/temp.txt"));
						boolean[] decodedMessageBits = Tools.toBits(decodedMessage);

						int sameBits = Tools.sameBits(decodedMessageBits, messageFileBits);
						similarity = (double) sameBits / decodedMessageBits.length;

						if (consoleOut != null) {
							consoleOut.append("Współczynnik zgodności: " + similarity + "\n");
							consoleOut.append("Zgodność bitów:  " + sameBits + "/" + decodedMessageBits.length + "\n");
						}

						// check if those block settings are better before increasing repetition
						if (repeats == 1) {
							if (sameBits > bestBitsSettings /*|| sameBytes > bestBytesSettings*/) {
								if (consoleOut != null) {
									consoleOut.append("Wykryto jak dotąd najlepsze ustawienia początkowe.\n");
								}


								bestBitsSettings = sameBits;

							} else if (sameBits != decodedMessageBits.length) {
								// those settings are useless, select another
								if (consoleOut != null) {
									consoleOut.append("Te ustawienia nie są dobre.\n");
								}

								continue allRetries;
							}
						}

						if (sameBits > bestBitsUltimate /*|| sameBytes > bestBytesUltimate*/) {
							if (consoleOut != null) {
								consoleOut.append("Wykryto najlepszą jak dotąd dokładność. Zapamiętuję.\n");
							}


							Files.copy(Paths.get("temp/temp.wav"), Paths.get("temp/best.wav"), StandardCopyOption.REPLACE_EXISTING);
							Files.copy(Paths.get("temp/temp.txt"), Paths.get("temp/bestMsg.txt"), StandardCopyOption.REPLACE_EXISTING);
							getKey().save("temp/bestKey.txt");

							bestBitsUltimate = sameBits;
							bestBytesUltimate = Tools.sameBytes(messageFile, decodedMessage);
						}

						// another method: increase repetition if best BYTE accuracy is found. Otherwise randomise parameters.
						if (sameBits == decodedMessageBits.length) {

							if (consoleOut != null) {
								consoleOut.append("Sukces!\n");
							}
							break allRetries;

						} else if (sameBits > bestBitsRepeats/* || sameBytes > bestBytesRepeats*/) {


							if (consoleOut != null) {
								consoleOut.append("Wykryto najlepszą dokładność bitową dla tych ustawień.\n");
							}

							bestBitsRepeats = sameBits;

							if (consoleOut != null) {
								consoleOut.append("Zwiększam ilość powtórzeń wiadomości...\n");
							}

							++repeats;

							continue;

						} else {
							// too low similarity for current repeats
							continue allRetries;
						}

					} catch (TooManyRetriesException e) {
						if (getMessageRepeat() == 1) {
							if (consoleOut != null) {
								consoleOut.append("Wiadomość jest za długa. Nie można zakodować w podanym nośniku.\n");
								return null;
							}
						}

						if (consoleOut != null) {
							consoleOut.append("Za dużo powtórzeń.\n");
						}
						continue allRetries;
					}
				}
				// !:setRepeat
			}
			// !:allRetries

			if (retry >= maxRetries && similarity < 1.0) {
				if (consoleOut != null) {
					consoleOut.append("Nie udało się bezblędnie zakodować wiadomości po " + maxRetries + " próbach.\n");
					consoleOut.append("Kodowanie z najlepszą znalezioną dokładnością.\n");
					consoleOut.append("Poprawnie zakodowane bity: " + bestBitsUltimate + " / " + messageFileBits.length + "(" + (100.0 * bestBitsUltimate / messageFileBits.length) +" %)\n");
					consoleOut.append("Poprawnie zakodowane bajty: " + bestBytesUltimate + " / " + messageFile.length + "(" + (100.0 * bestBytesUltimate / messageFile.length) +" %)\n");
				}
			}

			if (consoleOut != null) {
				consoleOut.append("Zakodowano po " + overallRetries + " próbach.\n\n");
			}

			setProgress(100);

			Files.copy(Paths.get("temp/best.wav"), Paths.get(finalOutputFileName), StandardCopyOption.REPLACE_EXISTING);
			Files.copy(Paths.get("temp/bestKey.txt"), Paths.get(getKeyFileName()), StandardCopyOption.REPLACE_EXISTING);

			setProperBits(bestBitsUltimate);
			setProperBytes(bestBytesUltimate);

		} catch (UnsupportedWaveFormatException | UnexpectedValueException | FileNotFoundException e) {
			if (consoleOut != null) {
				consoleOut.append(e.toString() + ": ");
				consoleOut.append(e.getMessage() + "\n");
			}
			System.err.println(e.getMessage());
			e.printStackTrace(System.err);
		} catch (IOException e) {
			if (consoleOut != null) {
				consoleOut.append(e.toString() + ": ");
				consoleOut.append(e.getMessage() + "\n");
			}
			System.err.println(e.getMessage());
			e.printStackTrace(System.err);
		} catch (Exception e) {
			if (consoleOut != null) {
				consoleOut.append(e.toString() + ": ");
				consoleOut.append(e.getMessage() + "\n");
			}
			System.err.println(e.getMessage());
			e.printStackTrace(System.err);
		}

		return null;
	}
}
